import logging
import re
import traceback
import socket
from queue import Queue
from threading import Lock, Condition
import threading

import requests

from app.tasks import dump_bytes


class NoConnection(Exception):
    pass


class NoReply(Exception):
    pass


class EndPoint:
    fields = re.compile("(\\S+)")
    value_response = re.compile("^\\d+\r\n")
    watch_response = re.compile("^[^=]+=[^=]+$")
    white_char = re.compile("\\s+")

    def __init__(self, proxy, host, port):
        self.logger = logging.getLogger("Proxy")
        self.name = "%s:%d" % (host, port)
        self.host = host
        self.port = port
        self.app = proxy
        self.responses = {
            # storage commands
            b'set ': self.response_storage,
            b'add ': self.response_storage,
            b'replace ': self.response_storage,
            b'append ': self.response_storage,
            b'prepend ': self.response_storage,
            b'cas ': self.response_storage,
            # retrieve commands
            b'get ': self.response_retrieval,
            b'gets ': self.response_retrieval,
            # deletion
            b'delete ': self.response_deletion,
            # Increment/Decrement
            b'incr ': self.response_increment_decrement,
            b'decr ': self.response_increment_decrement,
            # touch
            b'touch ': self.response_touch,
            # get and touch
            b'gat ': self.response_get_and_touch,
            b'gats ': self.response_get_and_touch,
            # slabs reassign
            b'slabs reassign ': self.response_slabs_reassign,
            # slabs automove
            b'slabs automove ': self.response_slabs_automove,
            # lru tuning
            b'lru ': self.response_lru_tuning,
            # lru crawler
            b'lru_crawler ': self.response_lru_crawler,
            # watchers
            b'watch ': self.response_watch,
            # stats
            b'stats\r\n': self.response_stats,
            b'stats ': self.response_stats,
            # other commands
            b'flush_all ': self.response_flush_all,
            b'cache_memlimit ': self.response_cache_memlimit,
            b'version\r\n': self.response_vision,
            b'verbosity ': self.response_verbosity,
            b'quit\r\n': self.response_quit,
            b'misbehave\r\n': self.response_misbehave
        }

        self.logging_extra = {'backend': self.name}

        self.__lock = Lock()
        self.__queue = []
        self.__working = Lock()

        self.sock = None
        self.buffer = b''
        self.closed = False
        self.health = True

    def task_running(self):
        return self.__working.locked()

    def append_request(self, request):
        self.__lock.acquire()
        self.__queue.append(request)
        size = len(self.__queue)
        self.__lock.release()
        # print("append",dump_bytes(request), 'to', self.name)
        return size

    def append_request_optional(self, request):
        self.__lock.acquire()
        size = len(self.__queue)
        if size == 0:
            self.__queue.append(request)
            size = 1
        self.__lock.release()
        # print("append",dump_bytes(request), 'to', self.name)
        return size

    def get_request_header(self):
        self.__lock.acquire()
        header = None if len(self.__queue) == 0 else self.__queue[0]
        self.__lock.release()
        return header

    def remove_request_header(self):
        self.__lock.acquire()
        if len(self.__queue):
            del self.__queue[0]
        self.__lock.release()

    def report_health(self):
        requests.post(self.app.base_url + "running/health", data=dict(backend=self.name, health=self.health))

    def open_connection(self, host, port):
        self.closed = False
        if self.sock is None:
            # try to open socket with try 3 times
            s = None
            for _ in range(2):
                try:
                    s = socket.create_connection((host, port), 10)
                    break
                except:
                    pass
            if s is None:
                s = socket.create_connection((host, port), 10)
            s.settimeout(20)
            self.sock = s

    def close_connection(self):
        if self.sock:
            try:
                self.sock.shutdown(2)
            except:
                pass
            try:
                self.sock.close()
            except:
                pass
            self.sock = None

    def shutdown(self):
        self.close_connection()
        self.closed = True

    def execute(self):
        reported_open_connection_err = False
        tries = 0
        resp = None
        self.__working.acquire()
        thread = threading.current_thread()
        print("backend.execute", self.name, 'thread', thread.getName())
        while True:
            try:
                request = self.get_request_header()
                if request is None:
                    # the queue is empty
                    break
                if self.closed:
                    resp = NoConnection("the connection was closed by proxy")
                    break
                # find out the handler of response
                handler = None
                for prefix, fn in self.responses.items():
                    if request.startswith(prefix):
                        handler = fn
                        break
                if handler is None:
                    # no handler of response for the request
                    self.logger.critical("不存在与请求%s对应的回应处理方法" % dump_bytes(request), extra=self.logging_extra)
                    resp = b"SERVER_ERROR proxy backend does NOT support this command\r\n"
                    self.remove_request_header()  # remove the request from queue as the request had been handled
                else:
                    # got the handler, send request to memcached and read the response
                    noreplay = EndPoint.no_relay(request)

                    try:
                        sent = self.send_request(request)
                        if sent:
                            # the request had been sent to memcached server
                            if noreplay:
                                resp = NoReply('no reply as command')
                                self.remove_request_header()  # remove the request from queue as the request had been handled
                            else:
                                try:
                                    resp = handler()
                                    self.remove_request_header()  # remove the request from queue as the request had been handled
                                    if not self.health:
                                        self.health = True
                                        self.report_health()
                                except ConnectionAbortedError as e:
                                    # the connection was disconnected
                                    reported_open_connection_err = False
                                    tries = 0
                                    self.close_connection()
                                except:
                                    # there is something wrong while reading from memcached and parsing the response
                                    self.logger.error("Memcached服务器读取请求%s的响应失败" % dump_bytes(request),
                                                      exc_info=True, extra=self.logging_extra)
                                    self.remove_request_header()  # remove the request from queue as the request had been handled
                                    reported_open_connection_err = False
                                    tries = 0
                                    self.close_connection()
                                    pass
                        else:
                            # the request can NOT been sent, we close current connect and try again
                            self.logger.error("发送请求%s到Memcached服务器失败" % dump_bytes(request), extra=self.logging_extra)
                            reported_open_connection_err = False
                            tries = 0
                            self.close_connection()
                    except:
                        # AS the exception can only come from self.open_connection, so, we will try again in next loop
                        if not reported_open_connection_err:
                            # report only once
                            reported_open_connection_err = True
                            if self.health:
                                self.logger.critical("连接Memcached服务器失败", exc_info=True, extra=self.logging_extra)
                        tries += 1
                        if tries > 2:
                            if self.health:
                                self.health = False
                                self.report_health()
                            resp = NoConnection("can NOT connect to memcached server")
                            break
            except:
                self.logger.critical("在与Memcached服务器交互时发生异常", exc_info=True, extra=self.logging_extra)
                break
        self.__working.release()
        print(self.name, 'got response', dump_bytes(resp))
        return resp

    def send_request(self, request):
        # send request to memcached server
        if self.sock is None:
            self.open_connection(self.host, self.port)
        try:
            self.sock.sendall(request)
            return True
        except Exception:
            return False

    def response_storage(self):
        # read the response of storage command
        line = self.readline()
        if line in (b"STORED\r\n", b"NOT_STORED\r\n", b"EXISTS\r\n", b"NOT_FOUND\r\n"):
            return line
        elif self.response_error(line):
            return line
        else:
            return b''.join((b"SERVER_ERROR proxy backend responses '", line[:-2], b"'\r\n"))

    def response_retrieval(self):
        items = []
        while True:
            line = self.readline()
            if line == b"END\r\n":
                items.append(line)
                return b''.join(items)
            elif self.response_error(line):
                items.append(line)
                return b''.join(items)
            elif line.startswith(b'VALUE '):
                # value line, followed by data block
                try:
                    fields = self.fields.findall(line.decode("utf-8"))
                    if len(fields) in [4, 5]:
                        size = int(fields[3]) + 2
                        data = self.readbytes(size)
                        items.append(line)
                        items.append(data)
                    else:
                        raise Exception("backend reads value error")
                except Exception as e:
                    traceback.print_exc()

    def response_deletion(self):
        # read the response of storage command
        line = self.readline()
        if line in (b"DELETED\r\n", b"NOT_FOUND\r\n"):
            return line
        elif self.response_error(line):
            return line
        else:
            return b''.join((b"SERVER_ERROR proxy backend responses '", line[:-2], b"'\r\n"))

    def response_increment_decrement(self):
        # read the response of storage command
        line = self.readline()
        if line == b"NOT_FOUND\r\n":
            return line
        elif self.response_error(line):
            return line
        else:
            if self.value_response.match(line.decode('utf-8')):
                return line
            else:
                return b''.join((b"SERVER_ERROR proxy backend responses '", line[:-2], b"'\r\n"))

    def response_touch(self):
        # read the response of storage command
        line = self.readline()
        if line in (b"TOUCHED\r\n", b"NOT_FOUND\r\n"):
            return line
        elif self.response_error(line):
            return line
        else:
            return b''.join((b"SERVER_ERROR proxy backend responses '", line[:-2], b"'\r\n"))

    def response_get_and_touch(self):
        items = []
        while True:
            line = self.readline()
            if line == b"END\r\n":
                items.append(line)
                return b''.join(items)
            elif self.response_error(line):
                items.append(line)
                return b''.join(items)
            elif line.startswith(b'VALUE '):
                # value line, followed by data block
                try:
                    fields = self.fields.findall(line.decode("utf-8"))
                    if len(fields) in [4, 5]:
                        size = int(fields[3]) + 2
                        data = self.readbytes(size)
                        items.append(line)
                        items.append(data)
                    else:
                        raise Exception("backend reads value error")
                except Exception as e:
                    traceback.print_exc()

    def response_slabs_reassign(self):
        # read the response of storage command
        line = self.readline()
        if line == b'OK\r\n':
            return line
        elif self.response_error(line):
            return line
        else:
            for prefix in (b'BUSY ', b'BADCLASS ', b'NOSPARE ', b'NOTFULL ', b'UNSAFE ', b'SAME '):
                if line.startswith(prefix):
                    return line
            return b''.join((b"SERVER_ERROR proxy backend responses '", line[:-2], b"'\r\n"))

    def response_slabs_automove(self):
        # read the response of storage command
        line = self.readline()
        if line in (b'OK\r\n', b'0\r\n', b'1\r\n', b'2\r\n'):
            return line
        elif self.response_error(line):
            return line
        else:
            return b''.join((b"SERVER_ERROR proxy backend responses '", line[:-2], b"'\r\n"))

    def response_lru_tuning(self):
        # read the response of storage command
        line = self.readline()
        if line == b'OK\r\n':
            return line
        elif self.response_error(line):
            return line
        else:
            return b''.join((b"SERVER_ERROR proxy backend responses '", line[:-2], b"'\r\n"))

    def response_lru_crawler(self):
        # read the response of storage command
        items = []
        while True:
            line = self.readline()
            if line == b'OK\r\n':
                items.append(line)
                return b''.join(items)
            elif self.response_error(line):
                items.append(line)
                return b''.join(items)
            else:
                for prefix in (b'BUSY ', b'BADCLASS '):
                    if line.startswith(prefix):
                        items.append(line)
                        return b''.join(items)
                # check the response for 'lru_crawler metadump'
                value_reply = True
                for field in line.decode('utf-8').split(' '):
                    if self.watch_response.match(field) is None:
                        value_reply = False
                        break
                    if self.white_char.match(field):
                        value_reply = False
                        break
                if value_reply:
                    items.append(line)
                else:
                    items.append(b''.join((b"SERVER_ERROR proxy backend responses '", line[:-2], b"'\r\n")))
                    return b''.join(items)

    def response_watch(self):
        # read the response of storage command
        line = self.readline()
        if line == b'OK\r\n':
            return line
        elif self.response_error(line):
            return line
        else:
            value_reply = True
            for field in line.decode('utf-8').split(' '):
                if self.watch_response.match(field) is None:
                    value_reply = False
                    break
                if self.white_char.match(field):
                    value_reply = False
                    break
            if value_reply:
                return line
            else:
                return b''.join((b"SERVER_ERROR proxy backend responses '", line[:-2], b"'\r\n"))

    def response_stats(self):
        items = []
        while True:
            line = self.readline()
            if line == b"END\r\n":
                items.append(line)
                return b''.join(items)
            elif self.response_error(line):
                items.append(line)
                return b''.join(items)
            elif line.startswith(b'STAT '):
                # value line, followed by data block
                items.append(line)
            else:
                items.append(b''.join((b"SERVER_ERROR proxy backend responses '", line[:-2], b"'\r\n")))
                return b''.join(items)

    def response_flush_all(self):
        # read the response of storage command
        line = self.readline()
        if line == b'OK\r\n':
            return line
        elif self.response_error(line):
            return line
        else:
            return b''.join((b"SERVER_ERROR proxy backend responses '", line[:-2], b"'\r\n"))

    def response_cache_memlimit(self):
        # read the response of storage command
        line = self.readline()
        if line == b'OK\r\n':
            return line
        elif self.response_error(line):
            return line
        else:
            return b''.join((b"SERVER_ERROR proxy backend responses '", line[:-2], b"'\r\n"))

    def response_vision(self):
        # read the response of storage command
        line = self.readline()
        if self.response_error(line):
            return line
        elif line.startswith(b'VERSION '):
            return line
        else:
            return b''.join((b"SERVER_ERROR proxy backend responses '", line[:-2], b"'\r\n"))

    def response_verbosity(self):
        # read the response of storage command
        line = self.readline()
        if line == b'OK\r\n':
            return line
        elif self.response_error(line):
            return line
        else:
            return b''.join((b"SERVER_ERROR proxy backend responses '", line[:-2], b"'\r\n"))

    def response_quit(self):
        # Upon receiving this command, the server closes the
        # connection. However, the client may also simply close the connection
        # when it no longer needs it, without issuing this command.
        pass

    def response_misbehave(self):
        line = self.readline()
        if line == b'OK\r\n':
            return line
        elif self.response_error(line):
            return line
        else:
            return b''.join((b"SERVER_ERROR proxy backend responses '", line[:-2], b"'\r\n"))

    def response_error(self, line):
        if line == b"ERROR\r\n":
            return True
        elif line.startswith(b"CLIENT_ERROR ") or line.startswith(b"SERVER_ERROR  "):
            return True
        return False

    def readline(self):
        while True:
            pos = self.buffer.find(b"\r\n")
            if pos >= 0:
                pos += 2
                line = self.buffer[:pos]
                self.buffer = self.buffer[pos:]
                return line
            else:
                data = self.sock.recv(4096)
                if len(data):
                    self.buffer += data
                else:
                    raise socket.error("disconnected")

    def readbytes(self, size):
        while True:
            if len(self.buffer) >= size:
                data = self.buffer[:size]
                self.buffer = self.buffer[size:]
                return data
            else:
                data = self.sock.recv(4096)
                if len(data):
                    self.buffer += data
                else:
                    raise socket.error("disconnected")

    @staticmethod
    def no_relay(data):
        p1 = data.find(b' noreply\r\n')
        if p1 > 0 :
            p2 = data.find(b'\r\n')
            if p2 > p1:
                return True
        return False


def run_backend(endpoint):

    return endpoint.execute()

if __name__ == "__main__":
    ep = EndPoint(None, "192.168.19.97", 60000)
    ep.queue.put(b"set key flags exptime 3\r\nabc\r\n")
    result = ep.execute()
    print(result)